package zdream.pmfield.stage;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import zdream.pmfield.data.foe.BStat;
import zdream.pmfield.stage.action.StageAction;
import zdream.pmfield.stage.object.FoeActor;

/**
 * <p>时间线, 也就是决定在 {@link Stage} 中, 每个 {@link FoeActor} 的行动顺序.
 * 本次时间线使用的是 Pokemon (传说·阿尔宙斯) 的顺序体系, 但不同的是, 每个 Foe 在轮到它的时候才选择行动方式,
 * 并非每回合开始时选择.
 * <p>特征如下:
 * <li>速度快的 {@link FoeActor} 可以每轮行动 1 到 2 次,
 * 但每回合确保每个 {@link FoeActor} 都至少行动一次.
 * <li>行动顺序可以展示在 UI 上;
 * </li>
 * </p>
 *
 * @author Zdream
 * @date 2023-11-11
 * @since 0.0.1
 */
public class Timeline {
	public final Stage stage;
	/**
	 * 当前回合数, 计算时需要向上取整
	 * @see Math#ceil(double)
	 */
	public float round;

	/**
	 * <p>具体算法:
	 * <ol>
	 * <li>开始时扫描所有参加的 foe, 记录最慢的那个的速度作为基准速度 basicSpeed
	 * <li>按照基准速度, 计算每个 foe 的行动耗费时间 {@link FoeActor#speedCost}
	 *    speedCost 为 1 时就是每回合行动 1 次;
	 *    speedCost 为 0.5 时就是每回合行动 (1 / 0.5 = 2) 次;
	 *    speedCost 越大, 行动次数越少;
	 *    speedCost 最小为 0.5, 最大为 1.5;
	 * <li>每次计算行动顺序时, 按照 foe 的 {@link FoeActor#speedCost} 计算顺序.
	 * </ol></p>
	 */
	private int basicSpeed;
	/**
	 * 指示当前 action
	 */
	private TimelineItem current;
	/**
	 * 指示当前 turn. 必须是 foe
	 */
	private FoeTimelineItem currentTurn;
	/**
	 * 所有剩余触发时间为 0 的按顺序加到 nextQueue 中.
	 */
	private final List<TimelineItem> nextQueue = new ArrayList<>();

	public Timeline(Stage stage) {
		this.stage = Objects.requireNonNull(stage);
	}

	public void init() {
		List<FoeActor> allFoes = stage.allFoes;
		basicSpeed = allFoes.stream()
				.mapToInt(foe -> foe.bStats[BStat.SP.ordinal()])
				.min().orElse(16);
		basicSpeed = Math.max(16, basicSpeed);
		for (FoeActor foe : allFoes) {
			short speed = foe.bStats[BStat.SP.ordinal()];
			foe.speedCost = calculateSpeedCost(speed);
			// 按照上面的计算, speedCost 范围在 0.5 ~ 1.0 (最开始的一批不会大于 1.0)
			// 为了让最慢的那只 foe 也能在第一回合发动, 把所有 foe 的行动提前 0.25 回合.
			foe.speedRemains = foe.speedCost - 0.25f;
		}

		// 按照当前计算的 speedCost 计算下面三回合内的行动顺序
	}

	public TimelineItem next() {
		final List<TimelineItem> items = createOrder().withCount(1).build();
		return items.get(0);
	}

	public TimelineItem untilNext() {
		if (!nextQueue.isEmpty()) {
			return current = nextQueue.remove(0);
		}

		final List<TimelineItem> items = createOrder().withCount(1).build();
		TimelineItem nextItem = items.get(0);

		float delta = nextItem.time - round;
		current = nextItem;
		round = nextItem.time;
		FoeActor foe = null;

		if (nextItem.isForFoe()) {
			currentTurn = nextItem.asFoe();
			foe = nextItem.asFoe().foe;
		} else if (nextItem.isForAction()) {
			buffItems.removeIf(item -> item == nextItem);
		}

		for (FoeActor foeActor : stage.allFoes) {
			if (foeActor == foe) {
				foeActor.speedRemains = 0;
			}
			foeActor.speedRemains = Math.max(foeActor.speedRemains - delta, 0);
		}

		return nextItem;
	}

	/**
	 * 当 Foe 没有其它行动点时才调用.
	 */
	public void flip() {
		if (currentTurn == null) {
			return;
		}

		FoeActor foe = currentTurn.foe;

		foe.speedRemains = foe.speedCost * foe.speedCostRate;
		foe.speedCostRate = 1f;
		currentTurn = null; // 主要是防止 flip 连续调用
		current = null;
	}

	/**
	 * @return (0.5 ~ 1.5)
	 */
	public float calculateSpeedCost(short speed) {
		return 1f - (float) (Math.atan((speed - basicSpeed) * 4f / basicSpeed) / Math.PI);
	}

	/**
	 * 如果一个 foe 在上一次行动完还有剩余的行动点 {@link FoeActor#actionPoint}
	 * 则说明它还能继续行动. 再把它的行动放在下一个. (但是可能其它状态先触发, 比如烧伤)
	 */
	public void putFoeWithRemainingActionPoint(FoeActor foe) {
		final FoeTimelineItem item = new FoeTimelineItem(foe, round);
		item.newTurn = false;
		nextQueue.add(item);
	}

	public boolean hasRemainingActionNow() {
		return !nextQueue.isEmpty();
	}

	/* ************
	 *  For Buff  *
	 ************ */

	private final List<ActionTimelineItem> buffItems = new ArrayList<>();

	public Supplier<Boolean> putActionWithTime(StageAction action, float time) {
		if (time <= round) {
			throw new IllegalArgumentException("触发时间 " + time + " 过小了. 现在时间是 " + round);
		}
		final ActionTimelineItem item = new ActionTimelineItem(action, time);
		buffItems.add(item);
		return () -> buffItems.remove(item);
	}

	public Supplier<Boolean> putActionNow(StageAction action) {
		final ActionTimelineItem item = new ActionTimelineItem(action, round);
		nextQueue.add(item);
		return () -> nextQueue.remove(item);
	}

	public ActionTimelineItem[] findAllActionItems() {
		return buffItems.stream().toArray(ActionTimelineItem[]::new);
	}

	/* ************
	 *   Sorter   *
	 ************ */

	public List<TimelineItem> createExpectOrder() {
		return createOrder().build();
	}

	public TimelineOrderBuilder createOrder() {
		return new TimelineOrderBuilder();
	}

	public abstract static class TimelineItem implements Comparable<TimelineItem> {
		/**
		 * 行动时间点 (回合数)
		 */
		public float time;
		public int innerId;

		public boolean isForFoe() {
			return false;
		}

		public FoeTimelineItem asFoe() {
			throw new IllegalStateException("It is not a FoeTimelineItem");
		}

		public boolean isForAction() {
			return false;
		}

		public ActionTimelineItem asAction() {
			throw new IllegalStateException("It is not a BuffTimelineItem");
		}

		@Override
		public int compareTo(TimelineItem o) {
			int result = Float.compare(this.time, o.time);
			return (result == 0) ? Integer.compare(this.innerId, o.innerId) : result;
		}
	}

	public static class FoeTimelineItem extends TimelineItem {
		public final FoeActor foe;
		public float cost;
		/**
		 * 如果一个 foe 在上一次行动完还有剩余的行动点 {@link FoeActor#actionPoint}
		 * 则说明它还能继续行动, 并把 newTurn 置为 false
		 */
		public boolean newTurn = true;

		private FoeTimelineItem(FoeActor foe, float roundNow) {
			this.foe = Objects.requireNonNull(foe);
			this.time = foe.speedRemains + roundNow;
			this.cost = foe.speedCost;
			this.innerId = foe.innerId;
		}

		private FoeTimelineItem(FoeTimelineItem item) {
			this.foe = item.foe;
			this.time = item.time;
			this.cost = foe.speedCost;
			this.newTurn = item.newTurn;
			this.innerId = foe.innerId;
		}

		@Override
		public boolean isForFoe() {
			return true;
		}

		@Override
		public FoeTimelineItem asFoe() {
			return this;
		}

		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder();
			builder.append(foe.foe.getName()).append('[').append(time).append(']');
			return builder.toString();
		}
	}

	public static class ActionTimelineItem extends TimelineItem {
		public final StageAction action;

		private ActionTimelineItem(StageAction action, float triggerTime) {
			this.action = Objects.requireNonNull(action);
			this.time = triggerTime;
			this.innerId = action.getInnerId();
		}

		@Override
		public boolean isForAction() {
			return true;
		}

		@Override
		public ActionTimelineItem asAction() {
			return this;
		}

		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder();
			builder.append(action).append('[').append(time).append(']');
			return builder.toString();
		}
	}

	public class TimelineOrderBuilder {
		int itemCount = 16;
		boolean enableAction = true;
		boolean foeNewTurnOnly = false;

		private TimelineOrderBuilder() {}

		public TimelineOrderBuilder withCount(int count) {
			this.itemCount = count;
			return this;
		}

		public TimelineOrderBuilder ignoreAction() {
			this.enableAction = false;
			return this;
		}

		public TimelineOrderBuilder newTurnOnlyForFoe() {
			this.foeNewTurnOnly = true;
			return this;
		}

		public List<TimelineItem> build() {
			List<TimelineItem> order = new ArrayList<>(itemCount);
			int need = itemCount;

			order.addAll(query(nextQueue, need));
			need = itemCount - order.size();
			if (need == 0) {
				return order;
			}

			final float[] roundNowWrapper = new float[] {round};
			List<TimelineItem> currents = stage.allFoes.stream()
					.filter(foe -> foe.seat != null)
					.map(foe -> new FoeTimelineItem(foe, roundNowWrapper[0]))
					.collect(Collectors.toList());
			currents.addAll(buffItems);

			float maxTime = round + 10;
			do {
				final TimelineItem next = Collections.min(currents);
				roundNowWrapper[0] = next.time;
				if (next.time > maxTime) {
					break; // avoid infinite loop
				}
				TimelineItem needCheck;

				if (next.isForFoe()) {
					final FoeTimelineItem nextFoe = next.asFoe();
					needCheck = new FoeTimelineItem(nextFoe);
					nextFoe.time += nextFoe.cost;
				} else {
					needCheck = (next);
					currents.remove(next);
				}

				if (accept(next)) {
					order.add(needCheck);
				}
			} while (order.size() < itemCount && !currents.isEmpty());

			return order;
		}

		private List<TimelineItem> query(List<TimelineItem> list, int count) {
			if (list.isEmpty()) {
				return Collections.emptyList();
			}

			return list.stream()
					.filter(this::accept)
					.limit(count)
					.collect(Collectors.toList());
		}

		private boolean accept(TimelineItem item) {
			if (!enableAction && item.isForAction()) {
				return false;
			}

			if (foeNewTurnOnly && item.isForFoe()) {
				return item.asFoe().newTurn;
			}
			return true;
		}
	}
}
